import {
    Object3D,
    Scene,
    GridHelper,
    AxesHelper,
    DirectionalLight,
    AmbientLight,
    PerspectiveCamera,
    Raycaster,
    Vector3,
    MOUSE,
    WebGLRenderer
} from "three";
import {OrbitControls} from 'three/examples/jsm/controls/OrbitControls'
import Overlay, {overlaySymbol} from "./Overlay";


class Map extends Overlay {
    private readonly _object: Object3D;
    private readonly _camera: PerspectiveCamera;
    private readonly _renderer: WebGLRenderer;
    private readonly _controls: OrbitControls;
    private readonly _objects: Object3D[] = [];

    constructor(dom: HTMLElement) {
        super()
        const $this = this
        const {clientWidth, clientHeight} = dom
        // 场景
        let scene = new Scene()
        let objects: Object3D[] = [] // 存放可激发事件的对象

        let gridHelper = new GridHelper(9000, 300, 0x303030, 0x303030)
        scene.add(gridHelper)

        let axesHelper = new AxesHelper(50)
        scene.add(axesHelper)

        let directionLight = new DirectionalLight(0xffffff, 1.0)
        directionLight.position.set(0, 300, 0)
        scene.add(directionLight)
        let ambientLight = new AmbientLight(0x444444, 1.0)
        scene.add(ambientLight)

        // 相机
        let camera = new PerspectiveCamera(45, clientWidth / clientHeight, 1, 10000)
        camera.position.set(0, 200, 0)

        // 渲染器
        let renderer = new WebGLRenderer({
            antialias: true,
        })
        renderer.setSize(clientWidth, clientHeight)
        renderer.setClearColor(0x202123, 1)
        const {domElement} = renderer
        dom.appendChild(domElement)

        function onResize() {
            const {clientWidth, clientHeight} = dom
            renderer.setSize(clientWidth, clientHeight)
            camera.aspect = clientWidth / clientHeight
            camera.updateProjectionMatrix()
            $this.render()
        }

        window.addEventListener('resize', onResize)

        function getIntersect(coords: { x: number; y: number }) {
            let ray = new Raycaster()
            ray.setFromCamera(coords, camera)
            let intersects = ray.intersectObjects(objects, true)
            // // 子元素优先选择（重要，不然同平面内父元素被选中导致子元素无法正确响应事件）
            // intersects.reverse().sort(function (a, b) {
            //   return a.distance - b.distance
            // })
            intersects.sort(function (a, b) {
                // @ts-ignore
                return b.object[overlaySymbol].zIndex - a.object[overlaySymbol].zIndex
            })
            return intersects.length > 0 ? intersects[0] : null
        }

        function getPoint(coords: { x: number; y: number }) {
            let {x, y} = coords
            let vec1 = new Vector3(x, y, 0).unproject(camera)
            let vec2 = new Vector3(x, y, 1).unproject(camera)

            let vy = 0
            let scale = (vy - vec1.y) / (vec2.y - vec1.y)
            let vx = vec1.x + (vec2.x - vec1.x) * scale
            let vz = vec1.z + (vec2.z - vec1.z) * scale
            return new Vector3(vx, vy, vz)
        }

        function getCoords(pageX: number, pageY: number) {
            let canvas = domElement
            let rect = canvas.getBoundingClientRect()
            let px = pageX - rect.left
            let py = pageY - rect.top
            let x = (px / canvas.clientWidth) * 2 - 1
            let y = -(py / canvas.clientHeight) * 2 + 1
            return {x, y}
        }

        function createMapEvent(event: MouseEvent) {
            let coords = getCoords(event.pageX, event.pageY)
            let point = getPoint(coords)
            let intersect = getIntersect(coords)
            // @ts-ignore
            let rawTarget = intersect ? intersect.object[overlaySymbol] : $this
            return {
                type: event.type,
                coords: coords,
                point: point,
                intersect: intersect,
                rawTarget: rawTarget,
                rawEvent: event,
                bubbled: true,
                stopPropagation() {
                    this.bubbled = false
                },
            }
        }

        function handleEvent(event: MouseEvent) {
            let evt = createMapEvent(event)
            evt.rawTarget.dispatchEvent(evt)
        }

        let lastRawTarget: Overlay = null

        function handleMouseMove(event: MouseEvent) {
            let evt = createMapEvent(event)
            let rawTarget = evt.rawTarget
            rawTarget.dispatchEvent(evt)

            if (rawTarget !== lastRawTarget) {
                if (lastRawTarget) {
                    let evt1 = {...evt}
                    evt1.type = 'mouseout'
                    lastRawTarget.dispatchEvent(evt1)
                }
                let evt2 = {...evt}
                evt2.type = 'mouseover'
                rawTarget.dispatchEvent(evt2)
                lastRawTarget = rawTarget
            }
        }

        function handleMouseOver(event: MouseEvent) {
            if (!lastRawTarget) {
                let evt = createMapEvent(event)
                let rawTarget = evt.rawTarget
                rawTarget.dispatchEvent(evt)
                lastRawTarget = rawTarget
            }
        }

        function handleMouseOut(event: MouseEvent) {
            if (lastRawTarget) {
                let evt = createMapEvent(event)
                lastRawTarget.dispatchEvent(evt)
                lastRawTarget = null
            }
        }

        function handleMouseDown(event: MouseEvent) {
            handleEvent(event)

            function onMouseUp(event: MouseEvent) {
                handleEvent(event)
                window.removeEventListener('mouseup', onMouseUp, false)
                window.removeEventListener('mousemove', handleMouseMove, false)
                domElement.addEventListener('mousemove', handleMouseMove, false)
                domElement.addEventListener('mouseup', handleEvent, false)
            }

            domElement.removeEventListener('mouseup', handleEvent, false)
            domElement.removeEventListener('mousemove', handleMouseMove, false)
            window.addEventListener('mousemove', handleMouseMove, false)
            window.addEventListener('mouseup', onMouseUp, false)
        }

        // 事件触发
        domElement.addEventListener('mousedown', handleMouseDown, false)
        domElement.addEventListener('mousemove', handleMouseMove, false)
        domElement.addEventListener('mouseup', handleEvent, false)
        domElement.addEventListener('mouseover', handleMouseOver, false)
        domElement.addEventListener('mouseout', handleMouseOut, false)
        // domElement.addEventListener('mouseenter', handleEvent, false)
        // domElement.addEventListener('mouseleave', handleEvent, false)
        domElement.addEventListener('click', handleEvent, false)
        domElement.addEventListener('dblclick', handleEvent, false)
        domElement.addEventListener('contextmenu', handleEvent, false)

        domElement.addEventListener('leftclick', handleEvent, false)
        domElement.addEventListener('rightclick', handleEvent, false)

        domElement.addEventListener('dragenter', handleEvent, false)
        domElement.addEventListener('dragover', handleEvent, false)
        domElement.addEventListener('dragleave', handleEvent, false)
        domElement.addEventListener('dragexit', handleEvent, false)
        domElement.addEventListener('drop', handleEvent, false)

        function onOrbitChange() {
            $this.render()
        }

        // 场景控制（注意：若在其它 mousedown 事件中需要设置禁用此控件，则其它事件需要在此控件实例化之前绑定事件，进而使本控件的 enabled:false 生效）
        let controls = new OrbitControls(camera, domElement)
        controls.mouseButtons.LEFT = MOUSE.PAN
        controls.mouseButtons.RIGHT = MOUSE.PAN
        controls.addEventListener('change', onOrbitChange)

        this.dispose = function () {
            controls.removeEventListener('change', onOrbitChange)
            // 解除绑定
            domElement.removeEventListener('drop', handleEvent, false)
            domElement.removeEventListener('dragexit', handleEvent, false)
            domElement.removeEventListener('dragleave', handleEvent, false)
            domElement.removeEventListener('dragover', handleEvent, false)
            domElement.removeEventListener('dragenter', handleEvent, false)

            domElement.removeEventListener('rightclick', handleEvent, false)
            domElement.removeEventListener('leftclick', handleEvent, false)

            domElement.removeEventListener('contextmenu', handleEvent, false)
            domElement.removeEventListener('dblclick', handleEvent, false)
            domElement.removeEventListener('click', handleEvent, false)
            // domElement.removeEventListener('mouseleave', handleEvent, false)
            // domElement.removeEventListener('mouseenter', handleEvent, false)
            domElement.removeEventListener('mouseout', handleMouseOut, false)
            domElement.removeEventListener('mouseover', handleMouseOver, false)
            domElement.removeEventListener('mouseup', handleEvent, false)
            domElement.removeEventListener('mousemove', handleMouseMove, false)
            domElement.removeEventListener('mousedown', handleMouseDown, false)

            // 其它
            window.removeEventListener('resize', onResize)
            dom.removeChild(domElement)
            renderer.dispose()
            objects.splice(0, objects.length)
            scene.dispose()
        }

        this._object = scene
        this._camera = camera
        this._renderer = renderer
        this._controls = controls
        this._objects = objects
    }

    protected get isMap() {
        return true
    }

    public setControlEnabled(enabled: boolean) {
        this._controls.enabled = enabled
    }

    public getObject() {
        return this._object
    }

    public render() {
        this._renderer.render(this.getObject(), this._camera)
    }

    public add(overlay: Overlay) {
        super.add(overlay)
        this._objects.push(overlay.getObject())
    }

    public remove(overlay: Overlay) {
        let objects = this._objects
        let index = objects.lastIndexOf(overlay.getObject())
        objects.splice(index, 1)
        super.remove(overlay)
    }
}

export default Map;
